AWS CDKでデプロイするときはキャッシュ(cdk.context.json)にも注目しよう

AWS CDKでデプロイするときはキャッシュ(cdk.context.json)にも注目しよう

Clock Icon2024.12.10

こんにちは。まるとです。
今回はAWS Cloud Development Kit (CDK、以下AWS CDKと表記)を利用して環境のデプロイを行う時につまづいたところがあったので、自身のメモとして記事を執筆します。

先に結論

  • AWS Systems Manager パラメータストアなどから値を取得する場合 cdk.context.json に値がキャッシュされている
  • AWS Systems Manager パラメータストアの値を変更した場合は、キャッシュクリアをお忘れなく
  • 複数人で作業する時は cdk.context.json もバージョン管理システムにコミットして、デプロイの一貫性を確保しよう

cdk.context.json とは

AWS CDKでスタックを合成(CloudFormationテンプレートを生成)する際に、AWS Systems Managerのパラメータストアの値など(以下、コンテキスト)をcdk.context.json に保存します。
コンテキストを cdk.context.json に保存することで、都度値をAWSに取りに行く必要がなくスタック合成の効率化はもちろん、再デプロイ時に一貫性を得ることができます。

cdk.context.json があると嬉しいこと

例えば、Amazon Linux 2023を利用したAmazon EC2を構築したい場合、以下のコードを書いたとします。
※シンプルにするため、本番等の利用は想定していないコードとなります。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class AppStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const vpc = new cdk.aws_ec2.Vpc(this, 'VPC', {
      maxAzs: 2,
    });

    const ec2SecurityGroup = new cdk.aws_ec2.SecurityGroup(this, 'SecurityGroup', {
      vpc,
      allowAllOutbound: true
    });

    const ec2 = new cdk.aws_ec2.Instance(this, 'Instance', {
      vpc,
      instanceType: cdk.aws_ec2.InstanceType.of(cdk.aws_ec2.InstanceClass.T3, cdk.aws_ec2.InstanceSize.MICRO),
      machineImage: cdk.aws_ec2.MachineImage.latestAmazonLinux2023(),   // Amazon Linux 2023の最新版を利用
      securityGroup: ec2SecurityGroup,
    });
  }
}

Amazon Linux 2023を利用したAmazon EC2 インスタンスを構築するために、コード内のmachineImagelatestAmazonLinux2023()を指定して、Amazon Linux 2023の最新のAMIを利用するようになっています。

    const ec2 = new cdk.aws_ec2.Instance(this, 'Instance', {
      vpc,
      instanceType: cdk.aws_ec2.InstanceType.of(cdk.aws_ec2.InstanceClass.T3, cdk.aws_ec2.InstanceSize.MICRO),
      machineImage: cdk.aws_ec2.MachineImage.latestAmazonLinux2023(),   // この時点ではAMI IDがami-aaaaaaaaaaaaaaaとする
      securityGroup: ec2SecurityGroup,
    });

この状態でプロジェクトメンバーであるAさんがデプロイを実行し、Amazon Linux 2023のEC2インスタンスが起動したとします。
ここで利用されたAmazon Linux 2023のAMIを(ami-aaaaaaaaaaaaaaa)とします。

その後、数週間してプロジェクトにBさんがアサインされました。
また、プロジェクト進行していく上で、EC2は削除保護有効にする要件が追加発生しました。

そこで、BさんはEC2 インスタンスの削除保護を有効にするため、以下のようにコードを更新しました。

    const ec2 = new cdk.aws_ec2.Instance(this, 'Instance', {
      vpc,
      instanceType: cdk.aws_ec2.InstanceType.of(cdk.aws_ec2.InstanceClass.T3, cdk.aws_ec2.InstanceSize.MICRO),
      machineImage: cdk.aws_ec2.MachineImage.latestAmazonLinux2023(),
      securityGroup: ec2SecurityGroup,
      disableApiTermination: true,  // 削除保護を追加
    });

コード更新後、Bさんはcdk deployを実行したところ、既存のEC2 インスタンスが置き換えられてしまいました。
一体なぜでしょうか。

これはAさんがcdk deployのタイミングで起動されたEC2インスタンスのAMI IDはami-aaaaaaaaaaaaaaaでしたが、Bさんがcdk deployしたタイミングではバージョンアップが行われ、AMI IDがami-bbbbbbbbbbbbbとAMIが変わっていたことにより、EC2インスタンスの置き換えが発生しました。

latestAmazonLinux2023()のコメントを確認しても以下の記載があります。

    /**
     * An Amazon Linux 2023 image that is automatically kept up-to-date
     *
     * This Machine Image automatically updates to the latest version on every
     * deployment. Be aware this will cause your instances to be replaced when a
     * new version of the image becomes available. Do not store stateful information
     * on the instance if you are using this image.
     */
    static latestAmazonLinux2023(props?: AmazonLinux2023ImageSsmParameterProps): IMachineImage;

このマシンイメージは、デプロイのたびに自動的に最新バージョンに更新されます。
そのため、新しいバージョンのイメージが利用可能になると、インスタンスが置き換えられることに注意してください。
このイメージを使用する場合、インスタンスに状態を保持する情報を保存しないでください。

このようにデプロイするタイミングで動作が異なるケースが発生することがありますが、値のキャッシュを適切に活用することで、意図しない変更を防ぐことが可能です。

そこでAWSから取得する値などをcdk.context.json にキャッシュして、キャッシュされた値を元にデプロイを実施することで、タイミング毎に差分が発生しない、決定的なデプロイを行うことができます。
その他、値を都度取得しに行く必要がなくなるため、スタックの合成が効率化/高速化します。

キャッシュしてみる

今回は一例として、AWS パラメータストアに値を保存して、取得&cdk.context.jsonにキャッシュしたいと思います。

1. AWS Systems Manager パラメータストアにパラメータを作成する

AWS Systems Manager パラメータストアでパラメータを作成します。

1

2

今回はパラメータ名を/test/maruto/hello、値はHello World!としました。

2. AWS CDKで値を取得してみる

AWS CDKで以下のように値を取得してコンソールに標準出力するようなコードを書きます。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class AppStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, {
      ...props,
      env: {
        account: process.env.CDK_DEFAULT_ACCOUNT,
        region: process.env.CDK_DEFAULT_REGION,
      },
    });

    const parameter = cdk.aws_ssm.StringParameter.valueFromLookup(this, '<パラメータの名前>');  // ここパラメータの値を入れる

    console.log(parameter);
  }
}

あとはcdk synthをしてみましょう。

dummy-value-for-/test/maruto/hello
Hello World!

dummy-value-for-<パラメータ名>という一時的な値が表示されますが、これはcdk synthを実行した時に以下の処理が行われているからです。

  1. 初回のStringParameter.valueFromLookup時はAWS Systems Managerから値を取得しようとしますが、このコード実行時点では値が取得できないため、ダミーの値を生成します。
  2. cdk synthのプロセスで動的な値が取得され、cdk.context.jsonに値がキャッシュされます。
  3. 値取得によりダミーの値との置き換えが発生するため、再度スタックの合成が実行されます。

初回のスタック合成ではダミーの値が使用され、実際の値が取得され次第再度スタックの合成が行われることから上記の出力になります。

また、実際の値が取得できるとcdk.context.jsonという値が生成されます。
ファイルの内容としては以下のようになっています。

{
  "ssm:account=<AWSアカウントID>:parameterName=<パラメータ名>:region=<リージョン>": "<値>"
}

この状態で再度cdk synthを実行すると、cdk.context.jsonからパラメータストアの値を取得するため、より早く実行が完了します。
また、スタックの合成も1度のみとなるため、同じコードでも以下の出力となります。

Hello World!

キャッシュ時の注意点

値をcdk.context.jsonにキャッシュし、キャッシュされた値を読み取ってスタックの合成をするため、以下のようなケースでは注意が必要です。

  • キャッシュ元の値(AWS Systems Managerのパラメータストアの内容など)を意図して変えた場合
  • 元々使用していたリージョンとは別のリージョンを利用する場合
  • 別のVPCを利用するなどスタックの依存関係に変更があった場合

etc...

全てに共通していることとして、参照先の値が変わるときがキャッシュをクリアするタイミングとなります。
cdk.context.jsonにキャッシュが存在する場合、動的な値(AWS Systems Manager パラメータストアなど)はキャッシュから読み取るため、実際の値が変わった場合はキャッシュのクリア、値の再取得が必要となります。

例えば、AWS Systems Manager パラメータストアに手動で作成したセキュリティグループのIDを保存していたとします。

/test/ec2/securityGroup : sg-xxxxxxxxxxxxxxxxx

の時にcdk synthを実行すると、上記の値がキャッシュされます。

ただし、何らかの事情でセキュリティグループを再作成する必要があったとします。
再作成した結果、セキュリティグループIDがsg-yyyyyyyyyyyyyyyyとなり、AWS Systems Manager パラメータストアに

/test/ec2/securityGroup : sg-yyyyyyyyyyyyyyyy

を設定しました。
この場合、実際の値がsg-yyyyyyyyyyyyyyyyにも関わらず、キャッシュではsg-xxxxxxxxxxxxxxxxxとなっているため、cdk synthcdk deployを実行しても古い値が使用されます。

この場合、使用すべき値を参照できず、期待した動作と異なる結果になる可能性があります。
そのため、パラメータストアなどの値を変えた場合は、キャッシュのクリアを行う必要があります。

キャッシュをクリアする場合は、cdk contextでキャッシュされている値を出力し、クリアしたい値の番号を指定してクリアします。

┌────┬─────────────────────────────┬─────────────────────┐
│ #  │ Key                         │ Value               │
├────┼─────────────────────────────┼─────────────────────┤
│ 1  │ oldKey                      │ "old value"         │
├────┼─────────────────────────────┼─────────────────────┤
│ 2  │ exampleKey                  │ "test value"        │
└────┴─────────────────────────────┴─────────────────────┘

上記の表の内、oldKeyを削除したい場合...

cdk context --reset 1

なお、すべてのキャッシュをクリアする場合は

cdk context --clear

まとめ

動的な値を参照してAWS CDKコードを書いている場合、特に複数人でコードの作成・保守をしていると同じコードなのに、「cdk diffの結果が異なる!」という事象が発生することがあります。
この場合、cdk.context.json(キャッシュ)の情報が異なっていないか確認しましょう。

また、複数人で作業をする場合は一貫性を得るために、cdk.context.jsonをGit等バージョン管理システムで管理しておくことをおすすめします。
※ AWS公式ドキュメントにも、アプリケーションの状態・デプロイの一貫性を得るためGitにコミットすることが重要としています。

https://docs.aws.amazon.com/cdk/v2/guide/context.html#context_construct

この記事が誰かの役に立てば幸いです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.